package game.poker.holdem.hand;

import game.poker.holdem.HoldEmUtils;
import game.poker.holdem.card.Card;
import game.poker.holdem.card.CommunityCards;
import game.poker.holdem.card.Rank;
import game.poker.holdem.card.RankComparator;
import game.poker.holdem.card.RankComparators;
import game.poker.holdem.entity.Player;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


public class BestHandExpert {
	private final static Map<Ranking, String> rankingValueMap;
	
	static {
		rankingValueMap = new HashMap<Ranking, String>();
		rankingValueMap.put(Ranking.HIGH_CARD, "1");
		rankingValueMap.put(Ranking.ONE_PAIR, "2");
		rankingValueMap.put(Ranking.TWO_PAIR, "3");
		rankingValueMap.put(Ranking.THREE_OF_A_KIND, "4");
		rankingValueMap.put(Ranking.STRAIGHT, "5");
		rankingValueMap.put(Ranking.FLUSH, "6");
		rankingValueMap.put(Ranking.FULL_HOUSE, "7");
		rankingValueMap.put(Ranking.FOUR_OF_A_KIND, "8");
		rankingValueMap.put(Ranking.STRAIGHT_FLUSH, "9");
		rankingValueMap.put(Ranking.ROYAL_FLUSH, "10");		
	}
	
	private final CommunityCards communityCards;
	private Player player;
	private List<Card> handCards;
	private Ranking handRanking;
	private RankComparator rankComparator;
	
	public BestHandExpert(CommunityCards communityCards) {
	    this.communityCards = communityCards;
	    reset();
	}
	
	private void reset() {
	    player = null;
	    handCards = new ArrayList<Card>();
	    handRanking = null;
	    rankComparator = RankComparators.ACE_HIGH;
	}
	
	private Long scoreHandCards() {
		String score = "";
		for (Card card : handCards) {
			Rank rank = card.getRank();
			int rankValue = rankComparator.getValue(rank);
			score += (rankValue < 10 ? "0" + rankValue : String.valueOf(rankValue));
		}
		String rankingValue = rankingValueMap.get(handRanking);		
		return Long.valueOf(rankingValue + score);

	}
	
	private void makeHand() { 
        Hand hand = new Hand(player, handCards, handRanking, scoreHandCards());
        player.setBestHand(hand);
        reset();
	}
	
	public void determineBestHandFor(Player player) {
	    this.player = player;
	    
	    /* Add all the community cards and player's hole cards in one list. 
	     * Total number of cards should be seven here. */
	    List<Card> cards = new ArrayList<Card>(communityCards.getAllCards());
	    cards.addAll(player.getHoleCards());
	    
	    /* Partition cards by distinct ranks, a list of cards for each unique rank. */
	    Map<Rank, List<Card>> rankCardsMap = HoldEmUtils.partitionRankWise(cards);
	    
	    /* Use the rank-wise partition to count the number of three-of-a-kinds and pairs. */
	    /* To collect the ranks in the map with three cards. */
	    List<Rank> threeOfAKindRanks = new ArrayList<Rank>(); 
	    /* To collect the ranks in the map with two cards. */
	    List<Rank> pairRanks = new ArrayList<Rank>(); 
	    /* Iterate over the entries in the rank-wise partition. */
	    for(Map.Entry<Rank, List<Card>> rankCardsE : rankCardsMap.entrySet()) {
	        /* List of cards for the current entry. */
	        List<Card> rankCards = rankCardsE.getValue();
	        
	        /* If there are 4 four cards of any rank there won't be any chance hands better than four-of-a-kind. */
	        if(rankCards.size() == 4) {
	            /* Current rank is the four-of-kind rank, just pick the highest from the remaining. */
	            List<Rank> otherRanks = new ArrayList<Rank>(rankCardsMap.keySet());
	            otherRanks.remove(rankCardsE.getKey());	            
	            Collections.sort(otherRanks, rankComparator);
	            Rank otherRank = otherRanks.get(otherRanks.size() - 1);
	            Card fifthCard = rankCardsMap.get(otherRank).get(0);
	            handCards.addAll(rankCards);	            
	            handCards.add(fifthCard);
	            handRanking = Ranking.FOUR_OF_A_KIND;
	            makeHand();
	            return;
	        }
	        /* Track the ranks with either 3 or 2 cards. */
	        if(rankCards.size() == 3) {
	            threeOfAKindRanks.add(rankCardsE.getKey());
	        } else if(rankCards.size() == 2) {
	            pairRanks.add(rankCardsE.getKey());
	        }
	    }
	    
	    /* At this point, it's certain that there is no four-of-a-kind. */
	    
	    /* If there is either two three-of-a-kinds or at least a three-of-a-kind and at least a pair,
	     * there won't be any chance of a hand better than a full-house.
	     */
	    if((threeOfAKindRanks.size() == 2) || (threeOfAKindRanks.size() == 1 && pairRanks.size() > 0)) {
	        /* If there are two ranks with three cards, need to pick the highest from the two. */
	        if(threeOfAKindRanks.size() == 2) {
	            Collections.sort(threeOfAKindRanks, rankComparator);
	        }
	        Rank threeOfAKindRank = threeOfAKindRanks.get(threeOfAKindRanks.size() - 1);
	        /* Find the highest pair to complete the full house. */
	        final Rank pairRank;
	        /* Since we are dealing with only seven cards, there can be only 3 possible scenarios here -
	         *     1. One more three-of-a-kind 
	         *     2. Only one pair
	         *     3. Two pairs
	         */
	        if(threeOfAKindRanks.size() == 2) {
	            pairRank = threeOfAKindRanks.get(0);
	        } else {
	            if(pairRanks.size() == 2) {
	                Collections.sort(pairRanks, rankComparator);
	            }
	            pairRank = pairRanks.get(pairRanks.size() - 1);
	        }
	        List<Card> threeOfAKindRankCards = rankCardsMap.get(threeOfAKindRank);
	        /* pairRank could also be a three-of-kind rank, so need to make sure only two cards are picked. */
	        List<Card> pairRankCards = rankCardsMap.get(pairRank).subList(0, 2);
	        handCards.addAll(threeOfAKindRankCards);	        
	        handCards.addAll(pairRankCards);
	        handRanking = Ranking.FULL_HOUSE;
	        makeHand();
	        return;
	    }
	    
	    /* At this point, it's certain that there is no four-of-a-kind, nor a full-house. */
	    /* This (slightly) alleviates the chances of better hands (i.e. royal-flush and straight-flush). */
	    
	    /* To check for xxxx-flush or flush (which will be the next best hand), need to inspect all the cards suit-wise. */
	     /* Partition the cards by each unique suits. */
	    /* Need to inspect the cards of the most frequent suit. */
	    List<Card> mostFrequentSuitCards = HoldEmUtils.mostFrequentSuitCards(cards);
	    
	    /* If there are at least 5 cards from the same suit then the best hand at this point is -
	     *     - at least a flush
	     *     - but could also be a straight-flush or a royal-flush (need to inspect rank-wise)
	     */
	    if(mostFrequentSuitCards.size() > 4) {
	        /* Map of rank to card for checking for straight and retrieve cards easily to make hand in case of straight.
	         * Don't need worry about duplicate ranks, since all the cards are from the same suit.
	         */
	        Map<Rank, Card> suitCardsByRankMap = new HashMap<Rank, Card>();
	        for(Card card : mostFrequentSuitCards) {
	            suitCardsByRankMap.put(card.getRank(), card);
	        }
	        List<Rank> suitRanks = new ArrayList<Rank>(suitCardsByRankMap.keySet());
	        /* Sort the ranks in Ace high manner. */
	        Collections.sort(suitRanks, rankComparator);	
	        /* look for the highest straight. */
	        int highStraightEndIndex = HoldEmUtils.endIndexOfHighestStraight(suitRanks);
	        /* if straight found - */
	        if(highStraightEndIndex > 0) {
	            /* if the highest rank within the straight is an Ace then it's a royal-flush, otherwise a straight-flush. */
	            if(suitRanks.get(highStraightEndIndex) == Rank.ACE) {
	                handRanking = Ranking.ROYAL_FLUSH;
	            } else {
	                handRanking = Ranking.STRAIGHT_FLUSH;
	            }
	            for(int i = highStraightEndIndex; i >= (highStraightEndIndex - 4); i--) {
	                handCards.add(suitCardsByRankMap.get(suitRanks.get(i)));
	            }
	            makeHand();
	            return;
	        }
	        /* There is no higher straight in the flush cards, 
	         * need to check for he lowest straight (A 2 3 4 5), only if there is an Ace. */
	        if(suitRanks.get(suitRanks.size() - 1) == Rank.ACE) {
	            /* create the list of the first 4 ranks form already sorted suit ranks */
	            List<Rank> lowRanks = new ArrayList<Rank>(suitRanks.subList(0, 4));
	            /* insert Ace rank at the beginning of the list. */
	            lowRanks.add(0, Rank.ACE);
	            /* check if the list is a straight. */
	            int endIndex = HoldEmUtils.endIndexOfHighestStraight(lowRanks, RankComparators.ACE_LOW);
	            /* If it's a straight */
	            if(endIndex > 0) {
	                /* make the Ace low comparator the rank comparator, rank comparator is also used for scoring hand. */
	                rankComparator = RankComparators.ACE_LOW;
	                handRanking = Ranking.STRAIGHT_FLUSH;
	                for(int i = (lowRanks.size() - 1); i >= 0; i--) {
	                    handCards.add(suitCardsByRankMap.get(lowRanks.get(i)));
	                }
	                makeHand();
	                return;
	            }
	        }
	        /* There is no royal-flush or straight-flush. So flush is the best possible hand at this point. */
	        handRanking = Ranking.FLUSH;
	        for(int i = (suitRanks.size() - 1); i >= (suitRanks.size() - 5); i--) {
                handCards.add(suitCardsByRankMap.get(suitRanks.get(i)));
            }
	        makeHand();
	        return;
	    }
	    
	    /* At this point it's certain that there is no chance of hand better than straight. */
	    /* Need to check for straight, only if there are a least 5 distinct ranks. */
	    if(rankCardsMap.size() > 4) {
	        /* create the list of all the distinct ranks. */
	        List<Rank> distinctRanks = new ArrayList<Rank>(rankCardsMap.keySet());
	        /* sort and check for the highest straight. */
	        Collections.sort(distinctRanks, rankComparator);	        
	        int endIndex = HoldEmUtils.endIndexOfHighestStraight(distinctRanks);	        
            if(endIndex > 0) {
                handRanking = Ranking.STRAIGHT;
                for(int i = endIndex; i >= (endIndex - 4); i--) {
                    handCards.add(rankCardsMap.get(distinctRanks.get(i)).get(0));
                }
                makeHand();
                return;
            }
            /* There is no higher straight, need to check for the lowest straight (A 2 3 4 5), only if the highest rank is Ace. */
            if(distinctRanks.get(distinctRanks.size() - 1) == Rank.ACE) {
                /* create the list of the first 4 ranks from the sorted list of distinct ranks. */
                List<Rank> lowRanks = new ArrayList<Rank>(distinctRanks.subList(0, 4));
                /* insert Ace rank at the beginning of the list. */
                lowRanks.add(0, Rank.ACE);
                /* check if straight */
                endIndex = HoldEmUtils.endIndexOfHighestStraight(distinctRanks, RankComparators.ACE_LOW);
                /* if straight */
                if(endIndex > 0) {
                    /* make the Ace low comparator the rank comparator, rank comparator is also used for scoring hand. */
                    rankComparator = RankComparators.ACE_LOW;
                    handRanking = Ranking.STRAIGHT;
                    for(int i = (lowRanks.size() - 1); i >= 0; i--) {
                        handCards.add(rankCardsMap.get(lowRanks.get(i)).get(0));
                    }
                    makeHand();
                    return;
                }
            }
	    }
	    /* At this point it's certain that straight is not possible. */
	    /* Next best possible hand is a three-of-a-kind, 
	     * which is possible only if we have three cards with same rank, and if true it's certain that it's the only and no pair exists. */
	    if(threeOfAKindRanks.size() == 1) {
	        Rank threeOfAKindRank = threeOfAKindRanks.get(0);
	        List<Rank> otherRanks = new ArrayList<Rank>(rankCardsMap.keySet());
	        otherRanks.remove(threeOfAKindRank);
	        Collections.sort(otherRanks, rankComparator);
	        handCards.addAll(rankCardsMap.get(threeOfAKindRank));
	        handCards.add(rankCardsMap.get(otherRanks.get(otherRanks.size() - 1)).get(0));
	        handCards.add(rankCardsMap.get(otherRanks.get(otherRanks.size() - 2)).get(0));
	        handRanking = Ranking.THREE_OF_A_KIND;
	        makeHand();
	        return;
	    }
	    /* it's a two-pair only if there are more than 1 pairs with same ranks. */
	    if(pairRanks.size() > 1) {
	        /* It's possible that there are three different pairs, so make sure to pick the two higher ones. */
	        Collections.sort(pairRanks, rankComparator);
	        List<Rank> twoPairRanks = pairRanks.subList(pairRanks.size() - 2, pairRanks.size());
	        List<Rank> otherRanks = new ArrayList<Rank>(rankCardsMap.keySet());
	        otherRanks.removeAll(twoPairRanks);
	        /* pick one from the highest of the remaining ranks. */
	        Collections.sort(otherRanks, rankComparator);	        
	        Rank otherRank = otherRanks.get(otherRanks.size() - 1);
	        handCards.addAll(rankCardsMap.get(twoPairRanks.get(1)));
            handCards.addAll(rankCardsMap.get(twoPairRanks.get(0)));
	        handCards.add(rankCardsMap.get(otherRank).get(0));
	        handRanking = Ranking.TWO_PAIR;
            makeHand();
	        return;
	    }
	    /* it's a one-pair only if there is one pair. */
	    if(pairRanks.size() == 1) {
	        Rank onePairRank = pairRanks.get(0);	        
	        List<Rank> otherRanks = new ArrayList<Rank>(rankCardsMap.keySet());
	        otherRanks.remove(onePairRank);
	        Collections.sort(otherRanks, rankComparator);
	        handCards.addAll(rankCardsMap.get(onePairRank));
	        /* pick the three higher ranks */
	        for(int i=otherRanks.size()-1; i>=otherRanks.size()-3; i--) {
	            handCards.add(rankCardsMap.get(otherRanks.get(i)).get(0));
	        }
	        handRanking = Ranking.ONE_PAIR;
            makeHand();
	        return;
	    }
	    /* It's a high card, it's certain that there is no better hand at this point. */
	    /* Just pick five higher ranks. */
	    List<Rank> ranks = new ArrayList<Rank>(rankCardsMap.keySet());
	    Collections.sort(ranks, rankComparator);	    
	    for(int i=ranks.size()-1; i>=ranks.size()-5; i--) {
            handCards.add(rankCardsMap.get(ranks.get(i)).get(0));
        }
	    handRanking = Ranking.HIGH_CARD;
	    makeHand();
	}
}
